package main

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"hash/crc32"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"strconv"
	"time"

	"github.com/gosuri/uilive"
	"golang.org/x/text/language"
	"golang.org/x/text/message"
	//"os/signal"
)

var serverIPString string
var serverIP net.IP
var receiveIndex = make([]byte, sliceOfBlock)
var receiveIndexZero = make([]byte, sliceOfBlock)
var processBeginBlockList = make(map[uint32]bool)
var processEndBlockList = make(map[uint32]bool)

const readBufferLength = 1 << 22

func client() {
	var err error
	if err = receiveMagic(); err != nil {
		log.Println("接收服务器信息错误: ", err)
		os.Exit(-1)
	}
	if fileName, err = getServerHttpReq("filename"); err != nil {
		fmt.Println("获取文件名错误: ", err)
		os.Exit(-1)
	}
	var n string
	if n, err = getServerHttpReq("filelength"); err != nil {
		fmt.Println("获取文件长度错误: ", err)
		os.Exit(-1)
	}
	if fileLength, err = strconv.ParseInt(n, 10, 64); err != nil {
		fmt.Println("文件长度不是合法的整数: ", err)
		os.Exit(-1)
	}
	fmt.Printf("%s服务端文件名 %s, 长度 %d\n", now(), fileName, fileLength)
	if _, err = getServerHttpReq("login"); err != nil {
		fmt.Println("向服务器登录错误: ", err)
	}
	fmt.Println(now(), "接收文件数据......")
	err = receiveFile()
	if err != nil {
		fmt.Println("接收文件数据错误: ", err)
		os.Exit(-1)
	}
	/*
		if len(lostList) > 0 {
			err = fixFile()
			if err != nil {
				fmt.Println("修复文件错误: ", err)
				os.Exit(-1)
			}
		}
	*/
	if OK, _, err := checkFileHash(); err != nil {
		log.Println("文件Hash计算错误: ", err)
	} else {
		if OK {
			fmt.Println(now(), "文件 Hash 校验正确")
			getServerHttpReq("verify?status=pass")
		} else {
			fmt.Println(now(), "文件 Hash 校验错误")
			getServerHttpReq("verify?status=fail")
		}
	}
	fmt.Println(now(), "文件接收完毕")
}

func getServerHttpReq(webPath string) (responseString string, err error) {
	webPath = "http://[" + serverIPString + "]:" + serverWebPort + "/" + webPath
	return getHttpReq(webPath)
}

// 接收服务端多播文件slice，然后组成一个block，检查是否有遗漏的slice，并记录，
// 然后保存block到文件，再向服务端发送完成标志。
func receiveFile() error {
	src := &net.UDPAddr{IP: broadcastGroup, Port: broadcastFilePort}

	//侦听适当的服务端口
	c, p, err := createConn(src)
	if err != nil {
		log.Println("链接错误: ", err)
		return err
	}
	defer c.Close()
	defer p.LeaveGroup(nil, src)
	c.SetReadBuffer(readBufferLength)

	byteCache := make([][]byte, 16)

	for i := range byteCache {
		byteCache[i] = make([]byte, 65536)
	}
	byteCacheIndex := 0
	data := byteCache[byteCacheIndex]
	writer := uilive.New()
	writer.Start()
	file, err := os.Create(filePath)
	if err != nil {
		log.Println("打开文件错误: ", err)
		return err
	}
	defer file.Close()

	printer := message.NewPrinter(language.Chinese)
	fileLengthString := printer.Sprintf("%d", fileLength)

	// 创建一个新的CRC32校验和计算器
	crc := crc32.New(crc32.MakeTable(crc32.IEEE))
	var breakLoop1 = false     //跳出外层循环
	var breakLoop2 = false     //跳出内层循环
	var totalSlices uint32 = 0 //总slice
	var lostSlices uint32 = 0  //遗漏slice
	var blockIndex uint32 = 0  //块编号
	var blockLength uint32 = 0 //块数据长度
	var totalRecieve int64 = 0 //总接收字节数
	startTime := time.Now()
	for {
		//接收切片到缓存
		for {
			breakLoop2 = false
			crc.Reset()
			n, _, _, err := p.ReadFrom(data)
			if err != nil {
				log.Println("接收文件数据错误: ", err)
			} else if n >= 16 {
				packetType := string(data[:4])
				//文件数据
				if packetType == dataString {
					sliceIndex := binary.LittleEndian.Uint32(data[4:8])
					readCrc32 := binary.LittleEndian.Uint32(data[12:16])
					crc.Write(data[16:n])
					crcValue := crc.Sum32()
					if readCrc32 == crcValue {
						//log.Println("保存数据:", sliceIndex)
						var readBlockIndex = sliceIndex >> (logBlock - logSlice) //读到的数据所属块
						if readBlockIndex == blockIndex {
							var data1 = data
							byteCacheIndex++
							if byteCacheIndex >= 16 {
								byteCacheIndex = 0
							}
							data = byteCache[byteCacheIndex]
							go func() {
								var pos = (sliceIndex & (sliceOfBlock - 1)) << logSlice
								copy(buffer[pos:pos+(uint32(n)-16)], data1[16:n])
								receiveIndex[sliceIndex&(sliceOfBlock-1)] = 1 //保存接收到的slice索引
							}()
						}
					}
				}
				//传送控制
				if packetType == infoString {
					infoType := string(data[4:8])
					//开始块
					if infoType == beginBlockString {
						blockIndex = binary.LittleEndian.Uint32(data[8:12])
						_, ok := processBeginBlockList[blockIndex]
						if !ok {
							blockLength = binary.LittleEndian.Uint32(data[12:16])
							totalRecieve += int64(blockLength)
							processBeginBlockList[blockIndex] = true
						}
					}
					//结束块
					if infoType == endBlockString {
						_, ok := processEndBlockList[blockIndex]
						if !ok {
							breakLoop2 = true
							slices := blockLength / sliceLength
							if blockLength%sliceLength > 0 {
								slices++
							}
							//fmt.Println("应接收slice块数", slices)
							totalSlices += slices
							//fmt.Println(receiveIndex[:slices])
							var lost uint32 = 0
							//var base = int(blockIndex) * sliceOfBlock
							var lostList = make([]int, 0) //遗漏slice编号集合
							for i, m := range receiveIndex {
								if uint32(i) >= slices {
									break
								}
								if m == 0 { //遗漏的包的处理
									lost++
									lostList = append(lostList, i)
								}
							}
							if len(lostList) > 0 {
								getAllLostSlice(lostList)
							}
							finishReceiveLostSlice()
							lostSlices += lost
							_, err := file.Write(buffer[:blockLength]) // 写缓存到文件
							if err != nil {
								log.Panicln("写入文件错误: ", err)
							}

							go file.Sync()

							copy(buffer, bufferZero)
							copy(receiveIndex, receiveIndexZero)
							processEndBlockList[blockIndex] = true

						}

					}
					//结束文件，需要增加重复接收信号处理
					if infoType == endFileString {
						//fmt.Println(now(), "处理文件结束过程")
						breakLoop1 = true
					}
				}
			}

			if breakLoop2 || breakLoop1 {
				break
			}
		}

		totalRecieveString := printer.Sprintf("%d", totalRecieve)
		var endTime = time.Now()
		//fmt.Printf("%v\n", endTime)
		duration := endTime.Sub(startTime)
		//fmt.Printf("%v\n", duration)
		usedTime := duration.Seconds()
		var l = 100 * float64(lostSlices) / float64(totalSlices)
		var v = float64(totalRecieve) / usedTime / 1000000.0
		go fmt.Fprintf(writer, "进度 %s / %s, 丢包率 %.4f%%, 传输速率 %.2f MB/s\n", totalRecieveString, fileLengthString, l, v)
		if l > 500 {
			fmt.Println("丢包率超过 5%, 请检查网络！")
			os.Exit(-1)
		}
		if breakLoop1 {
			break
		}

	}

	writer.Stop()
	fmt.Println("共计丢包: ", lostSlices)

	return nil
}

// 接收服务器识别信息,超时60秒退出
func receiveMagic() error {
	src := &net.UDPAddr{IP: broadcastGroup, Port: broadcastMagicPort}

	//侦听适当的服务端口
	c, p, err := createConn(src)
	if err != nil {
		log.Println("链接错误: ", err)
		return err
	}
	defer c.Close()
	defer p.LeaveGroup(nil, src)
	c.SetReadBuffer(readBufferLength)
	// 设置读取超时
	deadline := time.Now().Add(60 * time.Second)
	err = c.SetReadDeadline(deadline)
	if err != nil {
		log.Println("设置超时错误: ", err)
		return err
	}

	var data = make([]byte, 65536)
	for {
		n, _, src, err := p.ReadFrom(data)
		if err != nil {
			if os.IsTimeout(err) {
				fmt.Println("60 秒内未发现服务器, 程序中断！")
			}
			return err
		}
		receiveString := string(data[:n])
		if receiveString == magicNumber {
			udpAddr := src.(*net.UDPAddr)
			serverIP = udpAddr.IP
			serverIPString = serverIP.String()
			fmt.Println(now(), "发现服务器，地址: ", serverIPString)
			break
		}
	}

	return nil
}

// 块传送后向服务器登记缺失slice
func getAllLostSlice(lostList []int) error {
	for _, index := range lostList {
		data, err := getLostSlice(index)
		if err != nil {
			log.Println("获取缺失数据错误: ", err)
			continue
		} else {
			var pos = index * sliceLength
			copy(buffer[pos:], data[:])
		}
	}

	return nil
}

// 文件接收完成后，校验并重新获取校验错误的slice
func getLostSlice(i int) ([]byte, error) {
	webPath := "http://[" + serverIPString + "]:" + serverWebPort + "/getlostslice?index=" + strconv.Itoa(i)
	resp, err := http.Get(webPath)
	if err != nil {
		log.Fatal("接收数据错误:", err)
		return nil, err
	}
	defer resp.Body.Close()
	// 读取响应体的内容
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal("接收数据错误:", err)
		return nil, err
	}
	return data, nil
}

func finishReceiveLostSlice() error {
	webPath := "http://[" + serverIPString + "]:" + serverWebPort + "/finishreceivelostslice"
	resp, err := http.Get(webPath)
	if err != nil {
		log.Fatal("接收数据错误:", err)
		return err
	}
	defer resp.Body.Close()
	return nil
}

func checkFileHash() (bool, bool, error) {
	var fix = false
	fmt.Println(now(), "文件 Hash 校验...")
	var err error
	if fileHash, err = getServerHttpReq("filehash"); err != nil {
		log.Println("获取服务端文件 Hash 错误: ", err)
		return false, fix, err
	}
	sha256Hash := sha256.New()
	sha256Hash2 := sha256.New()
	file, err := os.Open(filePath)
	if err != nil {
		log.Println("打开文件错误: ", err)
		return false, fix, err
	}
	defer file.Close()
	var blockIndex int = 0
	var sliceIndex int = 0
	var currentPosition int64 = 0
	for {
		currentPosition, err = file.Seek(0, 1)
		if err != nil {
			log.Fatal(err)
		}
		nl, err := file.Read(buffer) // 读取文件内容到切片缓冲中
		if err != nil {
			if err == io.EOF {
				fileHash2 := hex.EncodeToString(sha256Hash.Sum(nil))
				if fileHash == fileHash2 {
					return true, fix, nil
				} else {
					//fmt.Println("Get Server Side File Hash: ", fileHash)
					//fmt.Println("Get Local File Hash: ", fileHash2)
					return false, fix, nil
				}
			}
			log.Println("打开文件错误: ", err)
			return false, fix, err
		} else {
			sha256Hash2.Reset()
			sha256Hash2.Write(buffer[:nl])
			blockHashString2 := hex.EncodeToString(sha256Hash2.Sum(nil))
			blockHashString, err := getServerHttpReq("blockhash?index=" + strconv.Itoa(blockIndex))
			if err != nil {
				log.Println("获取文件 Block Hash 错误: ", err)
			} else {

				if blockHashString != blockHashString2 {
					fix2, err := checkBlockHash(sliceIndex, nl)
					fix = fix || fix2
					if err != nil {
						log.Println("获取文件 Slice HASH 错误: ", err)
					}
					if fix2 {
						sha256Hash2.Reset()
						sha256Hash2.Write(buffer[:nl])
						blockHashString2 = hex.EncodeToString(sha256Hash2.Sum(nil))
						if blockHashString == blockHashString2 {
							file.Seek(currentPosition, 0)
							file.Write(buffer[:nl])
						}
					}
				}
				sha256Hash.Write(buffer[:nl])
			}
			blockIndex++
			sliceIndex += sliceOfBlock
		}
	}
}

func checkBlockHash(sliceIndex int, nl int) (bool, error) {
	var fix = false
	crc := crc32.New(crc32.MakeTable(crc32.IEEE))
	//fmt.Println("Check Block's Slice Start: ", sliceIndex)

	for i := 0; true; i++ {
		var s = i * sliceLength
		if s >= nl {
			break
		}
		var l int = sliceLength
		if (nl - s) < sliceLength {
			l = nl - s
		}

		crc.Reset()
		crc.Write(buffer[s : s+l])
		crcValue := crc.Sum32()

		sliceHashString2, err := getServerHttpReq("slicehash?index=" + strconv.Itoa(sliceIndex+i))
		if err != nil {
			return false, err
		} else {
			sliceHashString := fmt.Sprintf("%d", crcValue)
			if sliceHashString != sliceHashString2 {
				fmt.Println("Slice Hash错误: ", sliceIndex+i)
				/*
					data, err = getLostSlice(sliceIndex + i)
					if err != nil {
						fmt.Println("Get Slice Error: ", err)
						continue
					} else {
						crc.Reset()
						crc.Write(data)
						crcValue := crc.Sum32()
						sliceHashString = fmt.Sprintf("%d", crcValue)
						if sliceHashString == sliceHashString2 {
							copy(buffer[s:s+l], data[:l])
							fix = true
						}
					}
				*/
			}

		}
	}
	return fix, nil
}

func fixFile(lostList []int) error {
	fmt.Println(now(), "获取缺失Slice")
	file, err := os.OpenFile(filePath, os.O_RDWR, 0644)
	if err != nil {
		log.Println("打开文件错误: ", err)
		return err
	}
	defer file.Close()
	var i int = 0
	for _, index := range lostList {
		data, err := getLostSlice(index)
		//fmt.Println(now(), "=", i, ":", index, data[:8], err)
		if err != nil {
			continue
		}
		var pos = int64(index) * int64(sliceLength)
		file.Seek(pos, 0)
		file.Write(data)
		i++
	}
	file.Sync()
	return nil
}
